/*
 * Copyright 2017 Huawei Technologies Co., Ltd.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.openo.sdno.lcm.dispatcher.impl;

import java.util.List;
import java.util.logging.Logger;

import org.apache.http.HttpResponse;
import org.openo.sdno.lcm.dispatcher.Dispatcher;
import org.openo.sdno.lcm.dispatcher.RequestBodyMapper;
import org.openo.sdno.lcm.exception.ExternalComponentException;
import org.openo.sdno.lcm.exception.LcmInternalException;
import org.openo.sdno.lcm.genericclient.GenericApiClient;
import org.openo.sdno.lcm.model.workplan.WorkItem;
import org.openo.sdno.lcm.model.workplan.WorkPlan;
import org.openo.sdno.lcm.model.workplan.WorkPlanExecutionResult;
import org.openo.sdno.lcm.model.workplan.WorkPlanExecutionStrategy;
import org.openo.sdno.lcm.util.SwaggerUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.fasterxml.jackson.databind.JsonNode;

import io.swagger.models.HttpMethod;
import io.swagger.models.Swagger;
import io.swagger.models.parameters.HeaderParameter;
import io.swagger.models.parameters.PathParameter;
import io.swagger.models.parameters.QueryParameter;

@Component
public class DispatcherImpl implements Dispatcher {
    private final Logger log = Logger.getLogger("DispatcherImpl");

    private RequestBodyMapper requestBodyMapper; // request body mapper
    private GenericApiClient genericApiClient; // generic client to call rest
                                               // api

    /**
     * Dispatcher executes the work plan generated by Decomposer and returns the
     * execution results Notes: Work plan specifies the order of the tasks to be
     * executed. It also contains all information needed to fulfil each task so
     * that Dispatcher can be independent from other modules.
     *
     * @param plan
     *            the work plan generated by Decomposer and to be executed by
     *            the Dispatcher
     * @param strategy
     *            the strategy followed by the Dispatcher when one work item
     *            fails.
     * @return the results of executing the whole plan. It also contains the
     *         result for each task.
     */
    @Override
    public WorkPlanExecutionResult dispatch(final WorkPlan plan, final WorkPlanExecutionStrategy strategy) {
        // used to return execution result.
        WorkPlanExecutionResult result = new WorkPlanExecutionResult();
        result.setOverallResult(true);

        // execute work items one by one
        for (int i = 0; i < plan.size(); i++) {
            WorkItem item = plan.getWorkItem(i);

            // execute current work item
            boolean success = executeWorkItem(item);
            if (success) { // current work item is executed successfully
                result.addSucceededItem(item);
            } else { // current work item failed
                result.setOverallResult(false);
                result.addFailedItem(item);

                // stop if FAIL_FAST is specified, i.e., return after adding
                // remaining tasks to unprocessed list.
                if (strategy == WorkPlanExecutionStrategy.FAIL_FAST) {
                    for (int j = i + 1; j < plan.size(); j++) {
                        result.addUnprocessedItem(plan.getWorkItem(j));
                    }
                    return result;
                }
            }

            /**
             * sleep/cool-down between two work items TODO: remove after
             * demonstration is improved.
             */
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                log.severe("Sleep (between two work items) is interrupted unexpectedly!");
            }
        }
        return result;
    }

    /**
     * Execute one work item
     * 
     * @param workItem
     *            the work item to be executed
     * @return whether this work item has been executed successfully. true:
     *         success; false: failure
     */
    protected boolean executeWorkItem(WorkItem workItem) {
        // get contents from work item
        Swagger swagger = workItem.getSwaggerSpec();
        String apiUrl = workItem.getApiUrl();
        HttpMethod method = workItem.getMethod();

        // prepare headers to be sent in HttpRequest
        String contentTypeValue = SwaggerUtils.getConsumeFromSwagger(swagger, apiUrl, method);
        List<HeaderParameter> headerParameters = SwaggerUtils.getHeaderParametersFromSwagger(swagger, apiUrl, method);

        // prepare path and query parameters;
        List<QueryParameter> queryParameters = SwaggerUtils.getQueryParametersFromSwagger(swagger, apiUrl, method);
        List<PathParameter> pathParameters = SwaggerUtils.getPathParametersFromSwagger(swagger, apiUrl, method);

        // generate request body if needed
        JsonNode body = requestBodyMapper.map(workItem);

        try {
            // invoke API
            apiUrl = this.substituteIdPathParam(workItem);
            log.info(String.format("Start to call the following service API: %s; Method: %s", apiUrl,
                    method.toString()));

            HttpResponse response = genericApiClient.execute(apiUrl, method, contentTypeValue,
                    workItem.getNode().getProperties(), pathParameters, queryParameters, headerParameters, body);
            // put response message into work item
            workItem.setResponse(response.toString());

            log.info(String.format("Response from the service for API: %s; Method: %s, Response: %s", apiUrl,
                    method.toString(), response.toString()));

            // judge success or not and return;
            return isSuccessful(response);
        } catch (ExternalComponentException e) {
            log.severe(String.format("External service failed. Service API: %s; Method: %s, Exception: %s; ", apiUrl,
                    method.toString(), e.toString()));
            workItem.setResponse(e.toString());
            return false;
        } catch (LcmInternalException e) {
            log.severe(String.format("Dispatcher itself failed. Service API: %s; Method: %s, Exception: %s; ", apiUrl,
                    method.toString(), e.toString()));
            workItem.setResponse(e.toString());
            return false;
        }
    }

    public String substituteIdPathParam(WorkItem workItem) {

        String workitemNodeIdProperty = workItem.getNode().getPropertyValue("id");
        return workItem.getApiUrl().replaceAll("\\{[a-zA-Z_0-9]*\\}", workitemNodeIdProperty);
    }

    /**
     * judge whether api call is successful. Note: To be generic, we use only
     * HTTP status code to make the decision. In the future, we may should
     * analyze the response body if APIs are normalized.
     *
     * @param response
     *            HttpResponse from the called service
     * @return whether this call is successful.
     */
    protected boolean isSuccessful(HttpResponse response) {
        int statusCode = response.getStatusLine().getStatusCode();
        if ((statusCode >= 200) && (statusCode < 300)) {
            return true;
        } else {
            return false;
        }
    }

    @Autowired
    public void setRequestBodyMapper(RequestBodyMapper requestBodyMapper) {
        this.requestBodyMapper = requestBodyMapper;
    }

    @Autowired
    public void setGenericApiClient(GenericApiClient genericApiClient) {
        this.genericApiClient = genericApiClient;
    }
}